When you write queries against a database system in
SQL, specifying a set of columns to return is second nature. The goal is
to limit the columns returned to only those necessary for the query in
order to improve performance and limit network traffic (the less data
transferred, the better). This is achieved by listing the column names
after the Select clause in the following format. In most cases, only the columns of interest are returned using the SQL syntax form:
Select * from Contacts
Select ContactId, FirstName, LastName from Contacts
The first query will return
every column (and row) of the contacts table; the second will return
only the three columns explicitly listed (for every row), saving server
and network resources. The point is that the SQL language syntax allows a
different set of rows that does not match any existing database table,
view, or schema to be the structure used in returning data. Select
projections in LINQ query expressions allow us to achieve the same task.
If only few property values are needed in the result set, those
properties or fields are the only ones returned.
LINQ selection projections allow varied and powerful control over how and what data shape is returned from a query expression.
The different ways a select projection can return results are
As a single result value or element
In an IEnumerable<T> where T is of the same type as the source items
In an IEnumerable<T> where T is any existing type constructed in the select projection
In an IEnumerable<T> where T is an anonymous type created in the select projection
In an IEnumberable<IGrouping<TKey, TElement>>, which is a collection of grouped objects that share a common key
Each projection style has its use, and each style is explained by example in the following sections.
As with all good data-access
paradigms, the goal should be to return the fewest properties as
possible when defining a query result shape. This reduces the memory
footprint and makes the result set easier to code against because there
are fewer properties to wade through.
|
Return a Single Result Value or Element
Some of the standard query
operators return a single value as the result, or a single element from
the source collection; these operators are listed in Table 1.
Each of these operators end any cascading of results into another
query, and instead return a single result value or source element.
Table 1. Sample Set of Operators that Return a Specific Result Value Type
Return Type | Standard Query Operator |
---|
Numeric | Aggregate, Average, Max, Min, Sum, Count, LongCount |
Boolean | All, Any, Contains, SequenceEqual |
Type <T> | ElementAt, ElementAtOrDefault, First, FirstOrDefault, Last, LastOrDefault, Single, SingleOrDefault, DefaultIfEmpty |
As
an example, the following simple query returns the last element in the
integer array, writing the number 2 to the Console window:
int[] nums = new int[] { 5, 3, 4, 2 };
int last = nums.Last();
ConsoleWriteLine(last);
Return the Same Type as the Source—IEnumerable<TSource>
The most basic projection
type returns a filtered and ordered subset of the original source items.
This projection is achieved by specifying the range variable as the
argument after the select keyword. The following example returns an IEnumerable<Contact>, with the type Contact being inferred from the element type of the source collection:
List<Contact> contacts = Contact.SampleData();
IEnumerable<Contact> q = from c in contacts
select c;
A more appropriate query
would filter the results and order them in some convenient fashion. You
are still returning a collection of the same type, but the number of
elements and their order will be different.
List<Contact> contacts = Contact.SampleData();
IEnumerable<Contact> q = from c in contacts
where c.State == "WA"
orderby c.LastName,
c.FirstName ascending
select c;
Return a Different Type Than the Source—IEnumerable<TAny>
Any type can be projected as part of the select
clause, not just the source type. The target type can be any available
type that could otherwise be manually constructed with a plain new statement from the scope of code being written.
If the type
being constructed has a parameterized constructor containing all of the
parameters you specifically need, then you simply call that constructor.
If no constructor matches the parameter’s needed for this projection,
either create one or consider using the C# 3.0 type initializer syntax.
The benefit of using the new type initializer semantics is that it
frees you from having to define a specific constructor each time a new
projection signature is needed to cater for different query shapes. Listing 3-7 demonstrates how to project an IEnumerable<ContactName> using both constructor semantics.
Note
Resist the temptation to
overuse the type initializer syntax. It requires that all properties
being initialized through this syntax be read and write (have a getter
and setter). If a property should be read-only, don’t make it read/write
just for this feature.
Listing 1. Projecting to a collection of a new type—constructed using either a
specific constructor or by using type initializer syntax
List<Contact> contacts = Contact.SampleData();
// using a parameterized constructor
IEnumerable<ContactName> q1 =
from c in contacts
select new ContactName(
c.LastName + ", " + c.FirstName,
(DateTime.Now - c.DateOfBirth).Days / 365);
// using Type Initializer semantics
// note: The type requires a parameterless constructor
IEnumerable<ContactName> q2 =
from c in contacts
select new ContactName
{
FullName = c.LastName + ", " + c.FirstName,
YearsOfAge =
(DateTime.Now - c.DateOfBirth).Days / 365
};
// ContactName class definition
public class ContactName
{
public string FullName { get; set; }
public int YearsOfAge { get; set; }
// constructor needed for
// object initialization example
public ContactName() {
}
// constructor required for
// type projection example
public ContactName(string name, int age)
{
this.FullName = name;
this.YearsOfAge = age;
}
}
|
Return an Anonymous Type—IEnumerable<TAnonymous>
Anonymous
types is a new language feature introduced in C# 3.0 where the compiler
creates a type on-the-fly based on the object initialization expression
(the expression on the right side of the initial = sign). The following query demonstrates projecting to an IEnumerable<T> collection where T is an anonymous type:
List<Contact> contacts = Contact.SampleData();
var q = from c in contacts
select new
{
FullName = c.LastName + ", " + c.FirstName,
YearsOfAge =
(DateTime.Now - c.DateOfBirth).Days / 365
};
The anonymous type created in the previous example is composed of two properties, FullName and YearsOfAge.
Anonymous types free us from
having to write and maintain a specific type definition for every
different return result collection needed. The only drawback is that
these types are method-scoped and cannot be used outside of the method
they are declared by (unless passed as a System.Object type, but this is not recommended because property access to this object subsequently will need to use reflection).
Return a Set of Grouped Objects—IEnumerable<IGrouping<TKey,TElement>>
It is possible for LINQ to
Objects to group results that share common source values or any given
characteristic that can be equated with an expression using the group by query expression keyword or the GroupBy extension method.